﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using System.Security.Claims;
using System.Web;
using System.Web.Caching;
using LinqToOwin.Web.Labs.Data;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Owin;

namespace LinqToOwin.Web.Labs
{
  public static class Startup
  {
    private static readonly IReadOnlyCollection<string> nonHtmlPaths = new[] { "/styles", "/scripts", "/images", "/service" }.ToList().AsReadOnly();

    public static void Configuration(IAppBuilder app)
    {
      Contract.Requires(app != null);

      app.UseCookieAuthentication(new CookieAuthenticationOptions()
      {
        AuthenticationMode = AuthenticationMode.Active,
        AuthenticationType = "Forms",
        LoginPath = new PathString("/user/login"),
        LogoutPath = new PathString("/user/logout")
      });

      app.UseLinqToOwin(host =>
        Resources(host).Concat(
        Services(host).Concat(
        BeginHtml(host).Concat(
        AuthenticationPages(host["/user"]).Concat(
        UserPages(host["/user"]).Concat(
        AppPages(host).Concat(
        ErrorPages(host).Concat(
        EndHtml(host)))))))));
    }

    private static IEnumerable<IObservable<object>> Resources(LinqToOwinHost host)
    {
      yield return from context in host["/styles"].GetRequests select context.WithContentType("text/css", Styles());
      yield return from context in host["/scripts"].GetRequests select context.WithContentType("text/javascript", Scripts());
      yield return from context in host["/images"].GetRequests select context.WithContentType("image/png", Image(context));
    }

    private static IEnumerable<IObservable<object>> Services(LinqToOwinHost host)
    {
      yield return from context in host["/service"].GetRequests
                   select "<custom>"
                        + " <note>";

      yield return from context in host["/service"].GetRequests
                   select "   First responses that begin with XML, which definitely isn't valid HTML, are written with the Content-Type response header set to 'text/xml'.";

      yield return from context in host["/service"].GetRequests
                   select " </note>"
                        + "</custom>";
    }

    private static IEnumerable<IObservable<object>> BeginHtml(LinqToOwinHost host)
    {
      yield return from context in host.AllRequests
                   where !context.Handled
                   select context.NotHandled(Html.Begin(
                     head: @"<link rel=""stylesheet"" type=""text/css"" href=""/styles"" />"
                         + @"<script type=""text/javascript"" src=""http://ajax.aspnetcdn.com/ajax/jquery/jquery-2.1.1.min.js""></script>"
                         + @"<script type=""text/javascript"" src=""/scripts""></script>",
                     body: Navigation(context) + "<section>"));
    }

    private static IEnumerable<IObservable<object>> AuthenticationPages(LinqToOwinHost host)
    {
      yield return from context in host["/login"].GetRequests select LoginPage();
      yield return from context in host["/logout"].GetRequests select context.Do(() => context.Authentication.SignOut("Forms")).Redirect("~/");
      yield return from context in host["/login"].PostRequests
                   from form in context.Request.ReadFormAsync()
                   let userName = form["username"]
                   let password = form["password"]
                   select IsValid(userName, password)
                        ? context.SignIn(userName, "Forms", new Claim("FavoriteColor", "Green")).Redirect(context.Request.Query["ReturnUrl"] ?? "~/")
                        : LoginPage(failed: true);
    }

    private static IEnumerable<IObservable<object>> UserPages(LinqToOwinHost host)
    {
      yield return from context in host["/profile"].GetRequests
                   select context.IfAuthenticated(
                     user => ProfilePage(user),
                     () => context.Status(HttpStatusCode.Unauthorized).NoResponse());  // Auth middleware is set to "Active" mode, which automatically redirects on 401 errors.

      yield return from context in host["/profile"].PostRequests
                   from form in context.Request.ReadFormAsync()
                   let favoriteColor = form["favoriteColor"]
                   select context.IfAuthenticated(
                     user => context.Do(() => UpdateProfile(context, user, favoriteColor)).Redirect("/profile"),  // Redirect so the header is rendered with the latest favorite color.
                     () => context.Status(HttpStatusCode.Unauthorized).NoResponse());  // Auth middleware is set to "Active" mode, which automatically redirects on 401 errors.
    }

    private static IEnumerable<IObservable<object>> AppPages(LinqToOwinHost host)
    {
      yield return from context in host["/"].GetRequests select HomePage();
      yield return from context in host["/mashup"].GetRequests select MashupPage();

      yield return from context in host["/response"].PostRequests
                   from form in context.Request.ReadFormAsync()
                   let number = form.TryParseDecimal("number")
                   select NumberResponsePage(number);

      yield return from context in host["/response"].GetRequests
                   .Do(context => context.TraceOutput.WriteLine("Received invalid request for " + context.Request.Path + ".  Redirecting the user to the home page."))
                   select context.Redirect("~/");

      yield return from context in host["/hello"].GetRequests
                   let number = context.Request.Query.TryParseDecimal("number")
                   from i in Observable.Range(1, number.HasValue && number >= 1 && number <= 10 ? (int)number.Value : 0).DefaultIfEmpty(0)
                   select i == 0
                        ? @"Please " + Html.Link("/", "specify a number") + @" between 1 and 10."
                        : i + @". ""Hello LINQ to OWIN""<br/>";

      yield return from context in host["/mashup"].PostRequests
                   from form in context.Request.ReadFormAsync()
                   let cities = form.GetValues("city")
                   from weathers in WeatherClient.GetCurrentWeather(cities)
                   from zipped in
                     (from weather in weathers == null ? Observable.Empty<Weather>() : weathers.ToObservable()
                      select Observable.Zip(
                        (from tweets in TwitterClient.GetTweets(weather.Latitude, weather.Longitude).ToObservable()
                         from tweet in tweets
                         select tweet)
                         .ToList(),
                        GoogleMapsClient.GetMapAsync(weather.Latitude, weather.Longitude).ToObservable(),
                        (tweets, map) => new { tweets, map }))
                     .Merge()
                     .Aggregate(
                        new { tweets = Enumerable.Empty<IList<Tweet>>(), maps = Enumerable.Empty<byte[]>() },
                        (acc, cur) => new
                        {
                          tweets = (IEnumerable<IList<Tweet>>)acc.tweets.Concat(new[] { cur.tweets }),
                          maps = (IEnumerable<byte[]>)acc.maps.Concat(new[] { cur.map })
                        },
                        acc => new { tweets = from tweets in acc.tweets from tweet in tweets select tweet, maps = acc.maps })
                   select MashupPage(weathers, zipped.tweets, zipped.maps);
    }

    private static IEnumerable<IObservable<object>> ErrorPages(LinqToOwinHost host)
    {
      yield return from context in host.AllRequests
                   where !context.Handled
                   select context.Status(HttpStatusCode.NotFound, @"<em class=""error"">The page that you've requested was not found.</em>");
    }

    private static IEnumerable<IObservable<object>> EndHtml(LinqToOwinHost host)
    {
      yield return from context in host.AllRequests
                   where !context.IsRequestForAny(nonHtmlPaths)
                   select Html.End(body: "</section>");
    }

    private static string Styles()
    {
      return @"
html, body { margin: 0; padding: 0; }

nav { margin: 0 0 10px 0; padding: 2px; border-bottom: solid 1px black; }

  nav ul { margin-left: 10px; padding: 0;  }

li { display: inline; list-style: none; margin: 0; padding-right: 10px;  }

section { margin-left: 10px; }

.error { color: red; }

img { border: 0; }

section.mashup { width: 800px; margin: auto; border: solid 1px gray; }

.tweets { width: 320px; display: inline-block; float: right; padding: 5px; }

.tweet { margin-bottom: 7px; border: solid 1px azure; }

.city { width: 464px; display: inline-block; text-align: center; vertical-align: top; }

.map { width: 200px; height: 200px; display: inline-block; }

.weather { width: 260px; display: inline-block; vertical-align: top; }

.temp { font-weight: bold; font-size: 2em; }

button.add { font-weight: bold; font-size: 1.3em; }

.spacer { display: inline-block; width: 10px; }
";
    }

    private static string Scripts()
    {
      return @"
function addCityInput() {
  $(""#cities"").append(""<br/><input name='city' />"");
}
";
    }

    private static byte[] Image(LinqToOwinContext context)
    {
      var key = context.Request.Query["key"];

      return string.IsNullOrWhiteSpace(key) ? null : (byte[])HttpRuntime.Cache.Get(key);
    }

    private static string CacheImage(byte[] image)
    {
      var key = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);

      HttpRuntime.Cache.Add(key, image, null, Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(5), CacheItemPriority.Normal, null);

      return key;
    }

    private static string Navigation(LinqToOwinContext context)
    {
      var backgroundColor = context.IfAuthenticated(user =>
        (from claim in user.Claims
         where claim.Type == "FavoriteColor"
         select HttpUtility.HtmlEncode(claim.Value))
        .FirstOrDefault(),
        @"azure");

      return @"
<nav style=""background-color:" + backgroundColor + @""">
  <ul>
    <li>" + Html.Link("/", "Home") + @"</li>
    <li>" + Html.Link("/mashup", "Mashup") + @"</li>
    <li>" + Html.Link("/service", "XML-Web-Service") + @"</li>"
    + context.IfAuthenticated(() => @"<li>" + Html.Link("/user/profile", "Profile") + @"</li>") + @"
    <li>" + context.IfAuthenticated(() => Html.Link("/user/logout", "Logout"), () => Html.Link("/user/login", "Login")) + @"</li>
  </ul>
</nav>";
    }

    private static string HomePage()
    {
      return Html.Form(action: "/response", body: Html.TextField(name: "number", label: "Number:") + " " + Html.SubmitField());
    }

    private static string MashupPage(IEnumerable<Weather> weathers = null, IEnumerable<Tweet> tweets = null, IEnumerable<byte[]> maps = null)
    {
      var missingAny = weathers == null || tweets == null;

      return @"<h3>Enter the name of a city:</h3>"
        + Html.Form(body: @"<div id=""cities"">"
                        + Html.InputField(name: "city")
                        + "</div>"
                        + Html.ClientButtonField("+", "addCityInput();", @class: "add")
                        + "<br/><br/>"
                        + Html.SubmitField())
        + (missingAny ? null : @"
<section class=""mashup"">
  <div class=""tweets"">"
    + tweets.AggregateString((builder, tweet) => builder.Append(@"
    <div class=""tweet"">
      <strong>" + tweet.UserName + @"</strong> (<em>" + tweet.Location + @"</em>): <span>" + tweet.Text + @"</span>)
    </div>")) + @"
  </div>"
  + weathers.Zip(maps, (weather, map) => new { weather, map }).AggregateString((builder, pair) => builder.Append(@"
  <div class=""city"">
    <div class=""map"">" + Html.Image("/images?key=" + CacheImage(pair.map)) + @"</div>
    <div class=""weather"">
      <h3>" + pair.weather.City + ", " + pair.weather.State + @"</h3>
      <div class=""temp"">" + pair.weather.MinimumTemperature + "&deg;F - " + pair.weather.MaximumTemperature + @"&deg;F</div>
      <div>
      " + pair.weather.ConditionIcons.AggregateString(@"<span class=""spacer""></span>", (b, cur) => b.Append(Html.Image(cur.AbsoluteUri))) + @"
      </div>
    </div>
  </div>")) + @"
</section>");
    }

    private static string LoginPage(bool failed = false)
    {
      return Html.Form(body:
          (failed ? @"<div class=""error"">Invalid user name or password.</div>" : null)
        + Html.TextField("username", "User Name:") + "<br/>"
        + Html.PasswordField("password", "Password:") + "<br/>"
        + Html.SubmitField() + "<br/><br/>"
        + "<div>Use the following credentials to log in:</div>"
        + "<div><strong>User Name:</strong> test</div>"
        + "<div><strong>Password:</strong> password</div>");
    }

    private static bool IsValid(string userName, string password)
    {
      return userName == "test" && password == "password";
    }

    private static string ProfilePage(ClaimsPrincipal user)
    {
      var favoriteColor = (from claim in user.Claims
                           where claim.Type == "FavoriteColor"
                           select HttpUtility.HtmlEncode(claim.Value))
                           .FirstOrDefault();

      return Html.Form(body:
        Html.TextField("favoriteColor", "Favorite Color:", favoriteColor)
      + "<br/>"
      + Html.SubmitField("Save"));
    }

    private static void UpdateProfile(LinqToOwinContext context, ClaimsPrincipal user, string favoriteColor)
    {
      var claim = user.FindFirst("FavoriteColor");
      var identity = (ClaimsIdentity)user.Identity;

      identity.RemoveClaim(claim);
      identity.AddClaim(new Claim(claim.Type, favoriteColor));

      context.Authentication.SignIn(identity);
    }

    private static string NumberResponsePage(decimal? number)
    {
      return number.HasValue
           ? @"<div>You entered: <strong>" + number + @"</strong></div>
               <div>" + Html.Link("/hello?number=" + number, "Print &quot;Hello LINQ to OWIN&quot; " + number + " time(s)") + @" </div>"
           : @"<div>" + Html.Link("/", "Please enter a number!") + "</div>";
    }
  }
}